package snafu.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;

import snafu.framework.actions.AbstractAction;
import snafu.framework.actions.AbstractReaction;
import snafu.framework.agents.Agent;
import snafu.framework.agents.AgentItself;
import snafu.framework.objects.WorldObject;
import snafu.framework.objects.WorldObjectItself;
import snafu.framework.states.StateItself;
import snafu.framework.view.Access;
import snafu.framework.worlds.Position;
import snafu.framework.worlds.WorldMap;
import snafu.implementation.states.literals.lifetime.LifetimeLiteralItself;
import snafu.implementation.states.literals.position.PositionLiteralItself;
import snafu.implementation.states.literals.status.StatusLiteral;
import snafu.implementation.states.literals.status.StatusLiteral.Status;
import snafu.implementation.states.literals.status.StatusLiteralItself;


public class WorldController extends Observable {
	/** The map to simulate. */
	private WorldMap worldMap;
	/** The list agents that will act on the map. */
	private List<AgentItself> agents;
	/** The processor that handles the timing. */
	private Processor processor;

	/**
	 * Instantiates a new WorldController with a delay, the number of rounds to simulate and the
	 * world the simulation takes place in. Will create a new {@link Processor}.
	 * 
	 * @param delay
	 *            the milliseconds to wait after each step
	 * @param rounds
	 *            the number of rounds for the simulation
	 * @param worldPerception
	 *            the world map to simulate
	 */
	public WorldController(int delay, int rounds, WorldMap worldPerception) {
		this.worldMap = worldPerception;
		this.agents = new ArrayList<AgentItself>();
		
		processor = new Processor(delay, rounds) {
			@Override
			public void process() {
				nextStep();
			}
		};
	}
	
	
	/**
	 * Same as above, but will set the processor's rounds to infinity
	 * 
	 * @param delay
	 *            the milliseconds to wait after each step
	 * @param worldPerception
	 *            the world map to simulate
	 */
	public WorldController(int delay, WorldMap worldPerception) {
		this(delay, Processor.INFINITY, worldPerception);
	}
	
	/**
	 * Calculates the next step.
	 */
	private void nextStep() {
		// no agents left, stop run
		if (agents.size() == 0) {
			System.out.println(">> END OF SIMULATION, NO MORE AGENTS");
			processor.stop();
			return;
		}
		
		System.out.println(">> NEW ROUND");
		
		// shuffle the agents
		for (int i = 0; i < agents.size(); i++) {
			int newIndex = (int) (Math.random() * agents.size());
			AgentItself current = agents.get(i);
			AgentItself shuffle = agents.get(newIndex);
			agents.set(newIndex, current);
			agents.set(i, shuffle);
		}
		
		// let all agents act
		for (int i = 0; i < agents.size(); i++) {
			Agent agent = agents.get(i);
			
			// get the agents state and its current position in the world
			StateItself agentState = (StateItself) agent.getState();
			Position position = worldMap.getPosition(agent);
			
			// get the current status of the agent
			Status status = agentState.getLiteral(StatusLiteral.class);
			if (status == null) {
				agentState.setLiteral(new StatusLiteralItself(Status.ACTIVE, Access.PRIVATE));
				status = Status.ACTIVE;
			}
			
			switch (status) {
			// agent currently not active => skip it's turn
			case IDLE:
				continue;
			// agent was killed through prior action => remove it
			case DEAD:
				removeObject(agent);
				continue;
			}
			
			// create perception depending on the agents position
			WorldMap perception;
			if (position != null) {
				// create the perception for the agent
				perception = worldMap.getPerceptionFor(agent);
				
				// get the position for the agent in the perception and update it in its state
				Position perceptionPosition = perception.getPosition(agent);
				agentState.setLiteral(new PositionLiteralItself(perceptionPosition, Access.PRIVATE));
				
				// remove the agent from the perception
				perception.removeObject(agent);
			} else {
				// the agent isn't anywhere on the map => empty perception
				perception = null;
			}
			
			// get the action from the agent
			AbstractAction action = agent.act(perception);
			
			// restore the agents position
			agentState.setLiteral(new PositionLiteralItself(position, Access.PRIVATE));
			
			// no action returned => idle
			if (action == null) {
				System.out.println("   " + agent + " idles");
				continue;
			}

			// check applicability and execute the action
			if (action.isApplicable(worldMap)) {
				action.prepareAndExecute(worldMap);
				System.out.println("   " + action);
			} else {
				// TODO cheater!!!!
			}
			
			if (agentDead(agent)) {
				removeObject(agent);
				i--;
				continue;
			}
			
			// collect the reactions of all action targets
			List<AbstractReaction> reactions = new ArrayList<AbstractReaction>();
			for (WorldObject target : action.getTargets()) {
				AbstractReaction reaction = target.react(action);
				if (reaction != null) {
					reactions.add(reaction);
				}
			}
			
			// execute all that are executable
			for (AbstractReaction reaction : reactions) {
				if (reaction.isApplicable(worldMap)) {
					reaction.execute(worldMap);
					System.out.println("   " + reaction);
				}
				
				if (agentDead(agent)) {
					removeObject(agent);
					i--;
					break;
				}
			}
			
			
		} // end of agent loop
		
		// increase lifetime of all objects
		for (WorldObject object : worldMap.getObjects()) {
			
		}
		
		
		// notify observers
		setChanged();
		notifyObservers();
	}

	/**
	 * Checks whether an agent is dead or not. Will use the {@link StatusLiteral} for determining
	 * this.
	 * 
	 * @param agent
	 *            the agent to check
	 * @return true if the agent has a {@link StatusLiteral} with the value {@link Status#Dead},
	 *         false otherwise
	 */
	private boolean agentDead(Agent agent) {
		Status status = agent.getState().getLiteral(StatusLiteral.class);
		return status == Status.DEAD;
	}
	
	/**
	 * Adds the given object to the given position on the world map.
	 * 
	 * @param position
	 *            the object's position
	 * @param object
	 *            the object
	 */
	public void addObject(Position position, WorldObjectItself<?> object) {
		// add it to the world map
		worldMap.addObject(position, object);
		
		// get the state of the object and update it's position literal
		StateItself state = (StateItself) object.getState();
		state.setLiteral(new PositionLiteralItself(position, Access.PRIVATE));
		
		// set lifetime to 0
		state.setLiteral(new LifetimeLiteralItself(0, Access.PRIVATE));
		
		// if the object is an agent add it to the list of agents
		if (Agent.class.isAssignableFrom(object.getClass()))
			agents.add((AgentItself) object);
	}
	
	/**
	 * Fills the world map with a new instance of the given class.
	 * 
	 * @param objectClass
	 *            the class of the object to create
	 */
	public void fillMapWith(Class<? extends WorldObjectItself<?>> objectClass) {
		worldMap.fillWith(objectClass);
	}
	
	/**
	 * Removes the object from the worldmap and the list of agents.
	 * 
	 * @param object
	 *            the object to remove
	 */
	public void removeObject(WorldObject object) {
		worldMap.removeObject(object);
		agents.remove(object);
	}
	
	/**
	 * Returns the processor.
	 * 
	 * @return the processor
	 */
	public Processor getProcessor() {
		return processor;
	}
	
	/**
	 * Returns the world map.
	 * 
	 * @return the world map
	 */
	public WorldMap getWorldMap() {
		return worldMap;
	}
}
